#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <syslog.h>
#include <time.h>
#include <sys/time.h>
#include <assert.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdarg.h>
#include <unistd.h>

#include "ircjs.h"
#include "net.h"

#include "list.h"

#ifdef DEBUG
extern FILE* debuglog;
#endif

void set_address ( char* host, char* sport,
		   struct sockaddr_in* sap,
		   char* protocol ) {
  struct servent *sp;
  struct hostent* hp;
  char* endptr;
  short port;
  
  bzero ( sap, sizeof *sap );
  sap->sin_family = AF_INET;
  if ( host != NULL ) {
    if ( !inet_aton ( host, &sap->sin_addr ) ) {
      hp = gethostbyname ( host );
      if ( hp == NULL ) {}
      else {
	sap->sin_addr = *(struct in_addr*)hp->h_addr;
      }
    }
  } else 
    sap->sin_addr.s_addr = htonl ( INADDR_ANY );
  port = strtol ( sport, &endptr, 0 );
  if ( *endptr == '\0' )
    sap->sin_port = htons ( port );
  else {
    sp = getservbyname ( sport, protocol );
    if ( sp == NULL ) {}
    else sap->sin_port = sp->s_port;
  }
}
 
int udp_server ( char* host, char* port ) {
  int s = -1;
  struct sockaddr_in local;
  int i = 1;

  set_address ( host, port, &local, "udp" );
  s = socket ( AF_INET, SOCK_DGRAM, 0 );
  if ( setsockopt( s, SOL_SOCKET, SO_REUSEADDR, (void*) &i, sizeof(i) ) < 0 ) {
    syslog ( LOG_ERR, "%m setting SO_REUSEADDR on listen socket %d", s );
    return -1;
  }
  if ( bind ( s, (struct sockaddr*) &local, sizeof local ) ) 
    exit ( 5 );
  return s;
}

int create_tcp_listener ( char* host, char* port ) {
  struct sockaddr_in addr;
  int i;  
  int listen_fd = socket( AF_INET, SOCK_STREAM, 0 );

  if ( listen_fd < 0 ) {
    syslog ( LOG_CRIT, "creating listen socket for port: %s:%s", host, port );
    return -1;
  }
  
  (void) fcntl( listen_fd, F_SETFD, 1 );
  
  i = 1;
  if ( setsockopt( listen_fd, SOL_SOCKET, SO_REUSEADDR, (void*) &i, sizeof(i) ) < 0 ) {
    syslog ( LOG_ERR, "%m setting SO_REUSEADDR on listen socket %d", listen_fd );
    return -1;
  }
  
  set_address ( host, port, &addr, "tcp" );
  if ( bind( listen_fd, (struct sockaddr*)&addr, sizeof(struct sockaddr) ) < 0 ) {
    //    syslog ( LOG_CRIT, "%m binding listen socket %d", listen_fd );
    return -1;
  }
  
  if ( listen( listen_fd, 128 ) < 0 ) {
    syslog ( LOG_CRIT, "%m listening listen socket %d", listen_fd );
    return -1;
  }
  
#ifdef HAVE_ACCEPT_FILTERS
  {
    struct accept_filter_arg af;
    (void) bzero( &af, sizeof(af) );
    (void) strcpy( af.af_name, ACCEPT_FILTER_NAME );
    (void) setsockopt( listen_fd, SOL_SOCKET, SO_ACCEPTFILTER, (char*) &af, sizeof(af) );
  }
#endif /* HAVE_ACCEPT_FILTERS */
  
  return listen_fd;
}

enum CONNECTION_STATE { CONNECTING, CONNECTED, ERROR };
static struct tcp_connection {
  JSCList list;
  int sock;
  int state;
  char* name;
  struct sockaddr_in addr;
  struct {
    char data[1024 * 2];
    int offset;
  } rbuf;
} tcp_connections;

void init_net () {
  JS_INIT_CLIST(&tcp_connections.list);
}

char* get_tcp_connection_name ( void* c ) {
  struct tcp_connection* nc = (struct tcp_connection*) c;
  return nc->name;
}

extern void connected ( char* name, void* conn );
void create_tcp_connection ( char* name, char* host, char* port ) {
  struct tcp_connection* nc = 
    (struct tcp_connection*) calloc ( 1, sizeof ( struct tcp_connection ) );
  set_address ( host, port, &nc->addr, "tcp" );
  if ( ( nc->sock = socket ( AF_INET, SOCK_STREAM, 0 ) ) > 0 ) {
    nc->name = strdup ( name );
    fcntl ( nc->sock, F_SETFL, O_NONBLOCK );
    connect ( nc->sock, (struct sockaddr*)&nc->addr, sizeof nc->addr );
    nc->state = CONNECTING;
    JS_APPEND_LINK ( &nc->list, &tcp_connections.list );
  } else {
    free ( nc );
  }
}

void dump_connections () {
  fprintf ( stderr, "dumping connections: " );
  if ( tcp_connections.list.next == &tcp_connections.list ) {
    fprintf ( stderr, "list empty" );
  } else {
    JSCList* cl;
    for ( cl = tcp_connections.list.next; cl != &tcp_connections.list; cl = cl->next ) {
      struct tcp_connection* c = 
	// from /usr/include/linux/list.h
	((struct tcp_connection*)((char*)(cl) - 
				  (unsigned long)(&((struct tcp_connection*)0)->list)));
      fprintf ( stderr, "connections: %x %s", cl, c->name );
    }
  }
  fprintf ( stderr, "\n" );
}

int cleanup_tcp_connections_list () {
  JSCList* cl, * next;
  int n = 0;
  for ( cl = tcp_connections.list.next; cl != &tcp_connections.list; cl = next ) {
    next = cl->next;
    struct tcp_connection* c = 
      // from /usr/include/linux/list.h
      ((struct tcp_connection*)((char*)(cl) - 
				(unsigned long)(&((struct tcp_connection*)0)->list)));
    if ( c->state == ERROR ) {
      JS_REMOVE_LINK ( &c->list );
      free ( c->name );
      free ( c );
    } else {
      n++;
    }
  }
  return n;
}

void close_connection ( struct tcp_connection* conn, char* msg ) {
  if ( tcp_connections.list.next == &tcp_connections.list ) return;
  conn->state = ERROR;
}

#define SEND_BUF_SIZE 1024 * 4
static char static_send_buf[SEND_BUF_SIZE];
void tcp_send ( void* vc, char* fmt, ... ) {
  struct tcp_connection* conn = (struct tcp_connection*) vc;
  char* send_buf = static_send_buf;
  char* dynamic_send_buf = NULL;
  int nwritten = 0;
  int n = 0;
  int len = 0;

  va_list va;
  va_start ( va, fmt );
  nwritten = vsnprintf ( (char*)static_send_buf, SEND_BUF_SIZE, fmt, va );

  if ( nwritten >= SEND_BUF_SIZE ) {
    va_list va;
    if ( debug.system ) debug_out ( "NOTICE: tcp_send using dynamic buffer %d", nwritten );
    dynamic_send_buf = (char*) malloc ( nwritten + 1 );
    va_start ( va, fmt );
    nwritten = vsnprintf ( (char*)dynamic_send_buf, nwritten, fmt, va );
    va_end ( va );
    dynamic_send_buf[nwritten] = '\0';
    send_buf = dynamic_send_buf;
  }

  //  fprintf ( stderr, "SENDING: %s", send_buf );
  if ( debug.outgoing ) debug_out ( "SENDING: %s", send_buf );
  len = strlen ( (const char*)send_buf );
  if ( ( n = send ( conn->sock, send_buf, len, MSG_NOSIGNAL ) ) == len ) {
    if ( dynamic_send_buf != NULL ) {
      free(dynamic_send_buf);
      dynamic_send_buf = NULL;
    }
  } else {
    close_connection ( conn, "send" );
  }
}

extern void parse_irc_line ( char*, void* );
void do_tcp_connection_read ( struct tcp_connection* c ) {
  char* msg = c->rbuf.data;
  int offset = c->rbuf.offset;
  int r = recv ( c->sock, &msg[offset],
		 sizeof c->rbuf.data - offset, MSG_NOSIGNAL );
  if ( r > 0 ) {
#ifdef DEBUG
    fwrite ( &msg[offset], r, 1, debuglog );
#endif
    char* endofbuf = &msg[r + offset];
    char* sol = msg;
    char* eol;
    msg[r + offset] = '\0';
    c->rbuf.offset = 0;
	
    while ( ( eol = strchr ( sol, '\n' ) ) != NULL ) {
      *eol = '\0';
      if ( *(eol - 1) == '\r' ) *(eol - 1) = '\0';
      if ( debug.incoming ) debug_out ( "GOTLINE: %s\n", sol );
      parse_irc_line ( sol, (void*) c );
      sol = eol + 1;
    }
    if ( sol != endofbuf ) {
      // msg not drained
      offset = endofbuf - sol;
      memmove ( msg, sol, offset );
      c->rbuf.offset = offset;
    }
  } else {
    // closed by peer or an error, select said there was something to read
    close_connection ( c, "recv" );
  }
}
    
int net_dispatch () {
  fd_set rd, wr, er;
  FD_ZERO (&rd);
  FD_ZERO (&wr);
  FD_ZERO (&er);
  int nfds = 0;

  if ( tcp_connections.list.next == &tcp_connections.list ) return 0;  // no connections

  JSCList* cl;
  for ( cl = tcp_connections.list.next; cl != &tcp_connections.list; cl = cl->next ) {
    struct tcp_connection* c = 
      // from /usr/include/linux/list.h
      ((struct tcp_connection*)((char*)(cl) - 
				(unsigned long)(&((struct tcp_connection*)0)->list)));
    switch ( c->state ) {
    case CONNECTING:
      FD_SET ( c->sock, &wr );
      break;
    case CONNECTED:
      FD_SET ( c->sock, &rd );
      break;
    }
    FD_SET ( c->sock, &er );
    if ( c->sock > nfds ) nfds = c->sock + 1;
  }
    
  struct timeval select_timeout;
  select_timeout.tv_sec = 1;
  select_timeout.tv_usec = 0;
  int r = select ( nfds, &rd, &wr, &er, &select_timeout );
  if ( r == -1 && errno == EINTR ) {
  } else if ( r == -1 ) {
    // error on select
  } else {
    for ( cl = tcp_connections.list.next; cl != &tcp_connections.list; cl = cl->next ) {
      struct tcp_connection* c = 
	// from /usr/include/linux/list.h
	((struct tcp_connection*)((char*)(cl) - 
				  (unsigned long)(&((struct tcp_connection*)0)->list)));
      if ( FD_ISSET ( c->sock, &er ) ) {
	close_connection ( c, "select" );
      } else {
	switch ( c->state ) {
	case CONNECTING:
	  if ( FD_ISSET ( c->sock, &wr ) ) {
	    c->state = CONNECTED;
	    connected ( c->name, c );
	  }
	  break;
	case CONNECTED:
	  if ( FD_ISSET ( c->sock, &rd ) ) {
	    do_tcp_connection_read ( c );
	  }
	  break;
	}
      }
    }
  }
  return cleanup_tcp_connections_list();
}

void call_timer ( char* name, void* conn );
void call_timers () {
  JSCList* cl;
  for ( cl = tcp_connections.list.next; cl != &tcp_connections.list; cl = cl->next ) {
    struct tcp_connection* c = 
      // from /usr/include/linux/list.h
      ((struct tcp_connection*)((char*)(cl) - 
				(unsigned long)(&((struct tcp_connection*)0)->list)));
    call_timer ( c->name, c );
  }
}

